﻿// Copyright (c) 2012 Francis Xavier Joseph Pulikotil
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Daabli
{
    public abstract class Reader
    {
        // Types
        private struct CursorPositionInfo
        {
            public int DataIndex { get; private set; }
            public CursorPosition Position { get; private set; }

            public CursorPositionInfo(int iData, CursorPosition position)
                : this()
            {
                DataIndex = iData;
                Position = position;
            }
        }

        // Members
        private static /*const*/ readonly char[] _emptyData = { };

        private char[] _data = _emptyData;
        private int _iData = 0;
        private CursorPosition _cursorPosition = new CursorPosition(0, 0);
        private readonly Stack<CursorPositionInfo> _cursorPositionInfoStack = new Stack<CursorPositionInfo>();
        private readonly MessageLog _log = new MessageLog();
        private readonly Dictionary<Type, IObjectReader> _objectReaders = new Dictionary<Type, IObjectReader>();
        private readonly Dictionary<string, object> _namedObjects = new Dictionary<string, object>();
        private readonly Dictionary<string, Type> _namedTypes = new Dictionary<string, Type>();

        // Properties
        public bool AutoRegisterTypes { get; set; }
        public MessageLog Log { get { return _log; } }

        // Functions
        private static bool IsTypeInstantiable(Type type)
        {
            if (type == null)
                return false;

            if (type.IsValueTypeExt())
                return true;

            // Special case for string
            if (type == typeof(string))
                return true;

            if (type.IsInterfaceExt() || type.IsAbstractExt())
                return false;

            if (!type.HasPublicParameterlessConstructorExt())
                return false;

            return true;
        }

        public static void NoThrowAction(Action action)
        {
            try { action(); }
            catch (Exception) { }
        }
        public static object TryCreateObject(Type type)
        {
            if (type == null)
                return null;

            if (!IsTypeInstantiable(type))
                return null;

            // Special case for string
            if (type == typeof(string))
                return string.Empty;

            object result = null;
            NoThrowAction(() => { result = Activator.CreateInstance(type); });

            return result;
        }

        private static bool IsWhiteSpace(char ch)
        {
            char[] whiteSpaceChars = { ' ', '\r', '\n', '\t' };
            return whiteSpaceChars.Contains(ch);
        }
        private static bool IsTokenSeparator(char ch)
        {
            char[] tokenSeparators = { '=', ';', '"', '{', '}', ',' };
            return tokenSeparators.Contains(ch);
        }

        private bool CurrentCharExists
        {
            get { return _iData < _data.Length; }
        }
        private char CurrentChar
        {
            get { return _data[_iData]; }
            set { _data[_iData] = value; }
        }
        private char PreviousChar
        {
            get { return _data[_iData - 1]; }
            set { _data[_iData - 1] = value; }
        }
        private int CurrentCharIndex
        {
            get { return _iData; }
        }
        private void MoveToNextChar()
        {
            if (!CurrentCharExists)
                return;

            if (CurrentChar == '\n')
            {
                // Cursor moving to next line
                ++_cursorPosition.Line;
                _cursorPosition.Column = 1;
            }
            else
            {
                // Cursor moving to next column
                ++_cursorPosition.Column;
            }

            // Advance cursor
            ++_iData;
        }

        private bool RemoveComments()
        {
            if (!CurrentCharExists)
                return true;

            // Comments in strings are part of the string data, and are not to be removed.
            bool inString = (CurrentChar == '"');

            for (MoveToNextChar(); CurrentCharExists; MoveToNextChar())
            {
                // Keep track of whether we're inside a string or not
                //TODO: Bug - this won't work if there's an escaped backslash just before the end of the string. E.g. "\\"
                if ((CurrentChar == '"') && (PreviousChar != '\\'))
                    inString = !inString;

                // If we're inside a string, then don't find/remove comments.
                if (inString)
                    continue;

                if (PreviousChar != '/')
                    continue;

                // C++ style comment
                if (CurrentChar == '/')
                {
                    // Fill comment start (//) with whitespace
                    PreviousChar = CurrentChar = ' ';

                    // Fill with spaces until end of line
                    for (MoveToNextChar(); CurrentCharExists && (CurrentChar != '\n'); MoveToNextChar())
                        CurrentChar = ' ';

                    continue;
                }

                // C style comment
                if (CurrentChar == '*')
                {
                    // Set the starting location of the comment
                    _log.SetPosition(_cursorPosition.Line, _cursorPosition.Column - 1);

                    // Fill comment start (/*) with whitespace
                    PreviousChar = CurrentChar = ' ';

                    // Fill with spaces until pattern: */
                    char prevChar = default(char);
                    for (MoveToNextChar(); CurrentCharExists; MoveToNextChar())
                    {
                        char currChar = CurrentChar;

                        if (currChar != '\n')
                            CurrentChar = ' ';

                        if (prevChar == '*' && currChar == '/')
                            break;

                        prevChar = currChar;
                    }

                    if (!CurrentCharExists)
                    {
                        _log.Write(MessageType.Error, "comment not closed before end of file");
                        return false;
                    }

                    continue;
                }
            }

            return true;
        }

        public bool FromData(string data)
        {
            _data = string.IsNullOrEmpty(data) ? (_emptyData) : (data.ToCharArray());
            Rewind();

            if (!RemoveComments())
                return false;

            Rewind();
            return true;
        }
        public void Rewind()
        {
            _iData = 0;
            _cursorPosition = new CursorPosition(1, 1);
            _log.SetPosition(_cursorPosition);
            _namedObjects.Clear();
        }

        private void SkipWhiteSpace()
        {
            while (CurrentCharExists && IsWhiteSpace(CurrentChar))
                MoveToNextChar();
        }

        public bool ReadLiteral(ref string literal, char delimiter)
        {
            _log.SetPosition(_cursorPosition);

            int iStart = CurrentCharIndex;
            {
                while (CurrentCharExists && (CurrentChar != delimiter))
                    MoveToNextChar();
            }
            int iLength = CurrentCharIndex - iStart;

            if (iLength <= 0)
                return false;

            literal = new string(_data, iStart, iLength);
            Debug.Assert(literal.Length > 0);

            return true;
        }
        public bool ReadLiteral(ref char ch)
        {
            _log.SetPosition(_cursorPosition);

            if (!CurrentCharExists)
                return false;

            ch = CurrentChar;
            MoveToNextChar();

            return true;
        }

        public bool ReadExpectedLiteral(char expectedLiteral)
        {
            char literal = default(char);
            if (!ReadLiteral(ref literal))
                return false;

            return (literal == expectedLiteral);
        }

        public bool ReadToken(ref string token)
        {
            _log.SetPosition(_cursorPosition);

            SkipWhiteSpace();

            int iStart = CurrentCharIndex;
            {
                while (CurrentCharExists && !IsTokenSeparator(CurrentChar) && !IsWhiteSpace(CurrentChar))
                    MoveToNextChar();
            }
            int iLength = CurrentCharIndex - iStart;

            if (iLength <= 0)
                return false;

            token = new string(_data, iStart, iLength);
            Debug.Assert(token.Length > 0);

            return true;
        }
        public bool ReadToken(ref char ch)
        {
            _log.SetPosition(_cursorPosition);

            SkipWhiteSpace();

            return ReadLiteral(ref ch);
        }

        public bool ReadExpectedToken(string expectedToken)
        {
            string token = string.Empty;
            if (!ReadToken(ref token))
                return false;

            return (token == expectedToken);
        }
        public bool ReadExpectedToken(char expectedToken)
        {
            char token = default(char);
            if (!ReadToken(ref token))
                return false;

            return (token == expectedToken);
        }

        public bool ReadGroupHeader()
        {
            const char token = '{';
            if (ReadExpectedToken(token))
                return true;

            _log.Write(MessageType.Error, "'{0}' expected", token);
            return false;
        }
        public bool ReadGroupFooter()
        {
            const char token = '}';
            if (ReadExpectedToken(token))
                return true;

            _log.Write(MessageType.Error, "'{0}' expected", token);
            return false;
        }
        public bool ReadSeparator()
        {
            const char token = ',';
            if (ReadExpectedToken(token))
                return true;

            _log.Write(MessageType.Error, "'{0}' expected", token);
            return false;
        }

        private void SaveCursorPosition()
        {
            _cursorPositionInfoStack.Push(new CursorPositionInfo(_iData, _cursorPosition));
        }
        private void RestoreCursorPosition()
        {
            Debug.Assert(_cursorPositionInfoStack.Count > 0);
            var info = _cursorPositionInfoStack.Pop();

            _iData = info.DataIndex;
            _cursorPosition = info.Position;
        }

        public bool PeekExpectedToken(string expectedToken)
        {
            SaveCursorPosition();
            var result = ReadExpectedToken(expectedToken);

            RestoreCursorPosition();
            return result;
        }
        public bool PeekExpectedToken(char expectedToken)
        {
            SaveCursorPosition();
            var result = ReadExpectedToken(expectedToken);

            RestoreCursorPosition();
            return result;
        }

        public bool PeekGroupHeader()
        {
            return PeekExpectedToken('{');
        }
        public bool PeekGroupFooter()
        {
            return PeekExpectedToken('}');
        }
        public bool PeekSeparator()
        {
            return PeekExpectedToken(',');
        }

        private bool ReadGroupHeader(Grouping grouping, out bool bReadGroupFooter)
        {
            bReadGroupFooter = false;
            switch (grouping)
            {
                case Grouping.None:
                    break;

                case Grouping.Optional:
                    {
                        if (!PeekGroupHeader())
                            break;

                        if (!ReadGroupHeader())
                            return false;

                        bReadGroupFooter = true;
                    }
                    break;

                case Grouping.Required:
                    {
                        if (!ReadGroupHeader())
                            return false;

                        bReadGroupFooter = true;
                    }
                    break;

                default:
                    _log.Write(MessageType.InternalError, "invalid grouping");
                    return false;
            }
            return true;
        }

        private IObjectReader GetObjectReader(Type type)
        {
            // If an object reader is already registered for the specified type, then return it.
            {
                IObjectReader objectReader;
                if (_objectReaders.TryGetValue(type, out objectReader))
                    return objectReader;
            }

            // No object reader is registered; we'll try to register an appropriate one.

            if (!AutoRegisterTypes)
            {
                _log.Write(MessageType.Warning, "no object reader found for '{0}' and automatic registration of types is not enabled", type.ToString());
                return null; // We're not allowed to automatically register types.
            }

            // Is the type readable?
            var readableOfT = typeof(IReadable<>).MakeGenericType(type);
            if (readableOfT.IsAssignableFrom(type))
            {
                var objectReaderOfT = typeof(ObjectReader<>).MakeGenericType(type);
                var objectReader = Activator.CreateInstance(objectReaderOfT) as IObjectReader;

                if (!RegisterObjectReader(objectReader))
                    return null;

                return GetObjectReader(type);
            }

            // Is the type an enum?
            if (type.IsEnumExt())
            {
                var enumReaderOfT = typeof(EnumReader<>).MakeGenericType(type);
                var enumReader = Activator.CreateInstance(enumReaderOfT) as IObjectReader;

                if (!RegisterObjectReader(enumReader))
                    return null;

                return GetObjectReader(type);
            }

            return null;
        }
        private Type GetTypeFrom(string typeName)
        {
            if (string.IsNullOrEmpty(typeName))
                return null;

            // See if this is a custom name for a type
            Type namedType;
            if (_namedTypes.TryGetValue(typeName, out namedType))
                return namedType;

            // Try to create the type from the name.
            Type result = null;
            NoThrowAction(() => { result = Type.GetType(typeName); });

            return result;
        }

        public bool Read<T>(ref T value)
        {
            var valueType = (value != null) ? value.GetType() : typeof(T);

            // If a new object is being specified
            bool isDynamicallyCreatedObject = false;
            if (PeekExpectedToken('('))
            {
                if (!ReadExpectedToken('('))
                {
                    _log.Write(MessageType.Error, "type names must be enclosed in parenthesis");
                    return false;
                }

                string typeName = string.Empty;
                if (!ReadLiteral(ref typeName, ')'))
                {
                    _log.Write(MessageType.Error, "type name expected");
                    return false;
                }

                if (!ReadExpectedToken(')'))
                {
                    _log.Write(MessageType.Error, "type names must be enclosed in parenthesis");
                    return false;
                }

                Type newValueType = GetTypeFrom(typeName);
                if (newValueType == null)
                {
                    _log.Write(MessageType.Error, "'{0}' is not a recognized type", typeName);
                    return false;
                }

                if (!valueType.IsAssignableFrom(newValueType))
                {
                    _log.Write(MessageType.Error, "'{0}' is not a kind of '{1}'", newValueType, valueType);
                    return false;
                }

                var newValue = TryCreateObject(newValueType);
                if (newValue == null)
                {
                    _log.Write(MessageType.Error, "failed to create object of type '{0}'", newValueType);
                    return false;
                }

                value = (T)newValue;
                valueType = newValueType;

                isDynamicallyCreatedObject = true;
            }

            // If a name is being specified
            string registeredObjectName = string.Empty;
            if (PeekExpectedToken('['))
            {
                if (!ReadExpectedToken('['))
                {
                    _log.Write(MessageType.Error, "names must be enclosed in square brackets");
                    return false;
                }

                string name = string.Empty;
                if (!ReadLiteral(ref name, ']'))
                {
                    _log.Write(MessageType.Error, "name expected");
                    return false;
                }

                if (!ReadExpectedToken(']'))
                {
                    _log.Write(MessageType.Error, "names must be enclosed in square brackets");
                    return false;
                }

                if (isDynamicallyCreatedObject) // The object is being named
                {
                    if (_namedObjects.ContainsKey(name))
                    {
                        _log.Write(MessageType.Error, "names must be unique; duplicate name found '{0}'", name);
                        return false;
                    }

                    // Register the object name
                    _namedObjects.Add(name, null);

                    // Remember the object name for registering its value later.
                    // Note: We do not register the value itself now because its object reader
                    //       can return a whole new object and that must be registered instead.
                    registeredObjectName = name;
                }
                else // A previously named object is being referenced
                {
                    if (!_namedObjects.ContainsKey(name))
                    {
                        _log.Write(MessageType.Error, "unknown object '{0}' referenced", name);
                        return false;
                    }

                    var referencedObject = _namedObjects[name];
                    if (referencedObject == null)
                    {
                        _log.Write(MessageType.Error, "object named '{0}' is not yet completely loaded", name);
                        return false;
                    }

                    var referencedObjectType = referencedObject.GetType();
                    if (!valueType.IsAssignableFrom(referencedObjectType))
                    {
                        _log.Write(MessageType.Error, "'{0}' is not a kind of '{1}'", referencedObjectType, valueType);
                        return false;
                    }

                    value = (T)referencedObject;
                    valueType = referencedObjectType; //TODO: Is this statement necessary?

                    return true;
                }
            }

            // special case for string
            bool valueIsString = (valueType == typeof(string));

            if ((value == null) && !valueIsString)
            {
                _log.Write(MessageType.Error, "cannot read null object");
                return false;
            }

            if (valueType.IsGenericTypeExt())
                valueType = valueType.GetGenericTypeDefinition();

            var objectReader = GetObjectReader(valueType);
            if (objectReader == null)
            {
                _log.Write(MessageType.Error, "cannot read '{0}' - type has no IObjectReader registered for it", valueType.ToString());
                return false;
            }

            bool bReadGroupFooter;
            if (!ReadGroupHeader(objectReader.Grouping, out bReadGroupFooter))
                return false;

            var result = objectReader.Read(value, this);
            if (result == null)
            {
                _log.Write(MessageType.Error, "failed to read object");
                return false;
            }

            if (bReadGroupFooter && !ReadGroupFooter())
                return false;

            value = (T)result;

            // If this object was named, then register its value
            if (!string.IsNullOrEmpty(registeredObjectName))
            {
                Debug.Assert(_namedObjects.ContainsKey(registeredObjectName));
                _namedObjects[registeredObjectName] = value;
            }

            return true;
        }
        public bool Read<T>(string identifier, ref T value)
        {
            if (string.IsNullOrEmpty(identifier))
            {
                _log.Write(MessageType.InvalidArgument, "identifier cannot be empty");
                return false;
            }

            if (!ReadExpectedToken(identifier))
            {
                _log.Write(MessageType.Error, "'{0}' expected", identifier);
                return false;
            }

            if (!ReadExpectedToken('='))
            {
                _log.Write(MessageType.Error, "'=' expected");
                return false;
            }

            if (!Read(ref value))
                return false;

            if (!ReadExpectedToken(';'))
            {
                _log.Write(MessageType.Error, "';' expected");
                return false;
            }

            return true;
        }
        public bool Read<T>(string identifier, ref T value, T defaultValue)
        {
            if (string.IsNullOrEmpty(identifier))
            {
                _log.Write(MessageType.InvalidArgument, "identifier cannot be empty");
                return false;
            }

            if (!PeekExpectedToken(identifier))
            {
                value = defaultValue;
                return true;
            }

            return Read(identifier, ref value);
        }

        public bool ReadBase<T>(T refThis)
        {
            if (refThis == null)
            {
                _log.Write(MessageType.InvalidArgument, "cannot read base of null object");
                return false;
            }

            var searchType = typeof(T).GetBaseTypeExt();
            if (searchType.IsGenericTypeExt())
                searchType = searchType.GetGenericTypeDefinition();

            var objectReader = GetObjectReader(searchType);
            if (objectReader == null)
            {
                _log.Write(MessageType.Error, "cannot read '{0}' - type has no IObjectReader registered for it", searchType.ToString());
                return false;
            }

            bool bReadGroupFooter;
            if (!ReadGroupHeader(objectReader.Grouping, out bReadGroupFooter))
                return false;

            var result = objectReader.Read(refThis, this);
            if (result == null)
            {
                _log.Write(MessageType.Error, "failed to read object");
                return false;
            }

            if (bReadGroupFooter && !ReadGroupFooter())
                return false;

            return true;
        }

        public bool RegisterObjectReader(IObjectReader objectReader)
        {
            if (objectReader == null)
            {
                _log.Write(MessageType.InvalidArgument, "cannot register null object reader");
                return false;
            }

            if (_objectReaders.ContainsKey(objectReader.Type))
            {
                _log.Write(MessageType.Error, "object reader for '{0}' is already registered", objectReader.Type.ToString());
                return false;
            }

            _objectReaders.Add(objectReader.Type, objectReader);
            return true;
        }
        public bool UnregisterObjectReader(Type type)
        {
            if (type == null)
            {
                _log.Write(MessageType.InvalidArgument, "type cannot be null");
                return false;
            }

            if (!_objectReaders.ContainsKey(type))
            {
                _log.Write(MessageType.Error, "no object reader for '{0}' was registered", type.ToString());
                return false;
            }

            _objectReaders.Remove(type);
            return true;
        }
        public void UnregisterAllObjectReaders()
        {
            _objectReaders.Clear();
        }

        public bool RegisterTypeName(string typeName, Type type)
        {
            if (string.IsNullOrEmpty(typeName))
            {
                _log.Write(MessageType.InvalidArgument, "type name cannot be empty");
                return false;
            }

            if (type == null)
            {
                _log.Write(MessageType.InvalidArgument, "type cannot be null");
                return false;
            }

            if (_namedTypes.ContainsKey(typeName))
            {
                _log.Write(MessageType.Error, "the name '{0}' was already registered to type '{1}'", typeName, _namedTypes[typeName]);
                return false;
            }

            if (!IsTypeInstantiable(type))
            {
                _log.Write(MessageType.Error, "the type '{0}' being named, must be instantiable", type);
                return false;
            }

            _namedTypes.Add(typeName, type);
            return true;
        }
        public bool UnregisterTypeName(string typeName)
        {
            if (string.IsNullOrEmpty(typeName))
            {
                _log.Write(MessageType.InvalidArgument, "type name cannot be empty");
                return false;
            }

            if (!_namedTypes.ContainsKey(typeName))
            {
                _log.Write(MessageType.Error, "the name '{0}' was not registered to any type", typeName);
                return false;
            }

            _namedTypes.Remove(typeName);
            return true;
        }
        public void UnregisterAllTypeNames()
        {
            _namedTypes.Clear();
        }
    }
}
